package com.sx.core.mybatis;

import com.alibaba.druid.sql.PagerUtils;
import com.alibaba.druid.util.StringUtils;
import com.sx.core.dialects.Dialect;
import com.sx.core.dialects.MySqlDialect;
import com.sx.core.dialects.OracleDialect;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.MappedStatement.Builder;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.mapping.ResultMap;
import org.apache.ibatis.mapping.ResultMapping;
import org.apache.ibatis.mapping.SqlSource;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

@Intercepts({@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})})
public class PaginationInterceptor implements Interceptor {

    private static final Log logger = LogFactory.getLog(PaginationInterceptor.class);

    private static final int MAPPED_STATEMENT_INDEX = 0;

    private static final int PARAMETER_INDEX = 1;

    private static final int ROWBOUNDS_INDEX = 2;

    // mapper.xml中需要拦截的ID(正则匹配)
    private static String pageSqlId = "";
    private Dialect dialect;

    // 删除order by字句使用的正则表达式
    private static Pattern removeOrderByPattern = null;

    static {
        removeOrderByPattern = Pattern.compile("order\\s*by[\\w|\\W|\\s|\\S]*", Pattern.CASE_INSENSITIVE);
    }
    private static String MySQL="MySQL";
    private static String Oracle="Oracle";
    private static String SQLServer="Microsoft SQL Server";

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object[] args = invocation.getArgs();
        MappedStatement mappedStatement = (MappedStatement) args[MAPPED_STATEMENT_INDEX];
        Object parameter = args[PARAMETER_INDEX];
        RowBounds rowBounds = (RowBounds) args[ROWBOUNDS_INDEX];
        //自动加载数据库
        if(dialect==null){
            String databaseId= mappedStatement.getConfiguration().getDatabaseId();
            if(StringUtils.equals(MySQL,databaseId)){
                dialect=new MySqlDialect();
            }else if(StringUtils.equals(Oracle,databaseId)){
                dialect=new OracleDialect();
            }
        }

        // 拦截需要分页的SQL
//        if (!"*".equals(pageSqlId) && (!mappedStatement.getId().matches(pageSqlId))) {
//            return invocation.proceed();
//        }
        if(rowBounds!=null && (rowBounds.getLimit()!=RowBounds.NO_ROW_LIMIT || rowBounds.getOffset()!=RowBounds.NO_ROW_OFFSET)){

            BoundSql boundSql = mappedStatement.getBoundSql(parameter);
            String originalSql = boundSql.getSql().trim();
            if (originalSql == null || originalSql.isEmpty()) {
                return null;
            }
            Object parameterObject = boundSql.getParameterObject();

            // 兼容1.0 ~开始
            if (parameterObject instanceof Map) {
                ((Map<Object, Object>) parameterObject).put("QUERY_TOTAL", 0);
            }
            // 兼容1.0 ~结束

            String pageSql = dialect.buildPaginationSql(originalSql, rowBounds.getOffset(), rowBounds.getLimit());

//            logger.debug("生成分页SQL : " + formatSql(pageSql));

            args[ROWBOUNDS_INDEX] = new RowBounds(RowBounds.NO_ROW_OFFSET, RowBounds.NO_ROW_LIMIT);
            ReflectHelper.setValueByFieldName(boundSql, "sql", pageSql);
            args[MAPPED_STATEMENT_INDEX] = copyFromMappedStatement(mappedStatement, new BoundSqlSqlSource(boundSql),false);

        }else if (parameter instanceof CountParameter) {
            parameter = ((CountParameter) parameter).getParameter();
            BoundSql boundSql = mappedStatement.getBoundSql(parameter);

            String sql= PagerUtils.count(boundSql.getSql(),dialect.getType());

            BoundSql newBoundSql = new BoundSql(mappedStatement.getConfiguration(), sql, boundSql.getParameterMappings(), boundSql.getParameterObject());

            for(ParameterMapping parameterMapping:newBoundSql.getParameterMappings()){
                String property=parameterMapping.getProperty();
                Object val=boundSql.getAdditionalParameter(property);
                if(val!=null){
                    newBoundSql.setAdditionalParameter(property,boundSql.getAdditionalParameter(property));
                }
            }

            MappedStatement newMs = copyFromMappedStatement(mappedStatement, new BoundSqlSqlSource(newBoundSql), true);
            args[MAPPED_STATEMENT_INDEX] = newMs;
            args[PARAMETER_INDEX] = parameter;
        }
        return invocation.proceed();
    }


    public static class CountParameter {
        private Object parameter;

        public CountParameter(Object parameter) {
            this.parameter = parameter;
        }

        public Object getParameter() {
            return parameter;
        }
    }

    private MappedStatement copyFromMappedStatement(MappedStatement ms, SqlSource newSqlSource,boolean isCount) {
        Builder builder = new Builder(ms.getConfiguration(), ms.getId(), newSqlSource, ms.getSqlCommandType());

        builder.resource(ms.getResource());
        builder.fetchSize(ms.getFetchSize());
        builder.statementType(ms.getStatementType());
        builder.keyGenerator(ms.getKeyGenerator());

        builder.timeout(ms.getTimeout());

        builder.parameterMap(ms.getParameterMap());
        if (isCount) {
            List<ResultMap> resultMaps = new ArrayList<ResultMap>();
            resultMaps.add(new ResultMap.Builder(ms.getConfiguration(), ms.getId(), Long.class, new ArrayList<ResultMapping>()).build());
            builder.resultMaps(resultMaps);
        } else {
            builder.resultMaps(ms.getResultMaps());
        }

        builder.resultSetType(ms.getResultSetType());

        builder.cache(ms.getCache());
        builder.flushCacheRequired(ms.isFlushCacheRequired());
        builder.useCache(ms.isUseCache());

        return builder.build();
    }

    private String removeOrderBy(String sql) {
        if (sql == null || sql.isEmpty()) {
            return "";
        }
        Matcher matcher = removeOrderByPattern.matcher(sql);
        StringBuffer buffer = new StringBuffer();
        while (matcher.find()) {
            matcher.appendReplacement(buffer, "");
        }
        matcher.appendTail(buffer);
        return buffer.toString();
    }

    private String formatSql(String sql) {
        if (sql == null || sql.isEmpty()) {
            return "";
        } else {
            return sql.replace("\r", " ").replace("\n", " ").replace("\n\r", " ").replace("\t", " ");
        }
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties prop) {
        pageSqlId = prop.getProperty("pageSqlId", "*").trim();
//        String dialectClass = prop.getProperty("dialectClass");
//        try {
//            dialect = (Dialect) Class.forName(dialectClass).newInstance();
//        } catch (Exception e) {
//            throw new RuntimeException("cannot create dialect instance by dialectClass:" + dialectClass, e);
//        }
    }

    private static class BoundSqlSqlSource implements SqlSource {

        BoundSql boundSql;

        public BoundSqlSqlSource(BoundSql boundSql) {
            this.boundSql = boundSql;
        }

        public BoundSql getBoundSql(Object parameterObject) {
            return boundSql;
        }
    }
}
